{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit Playlist;

interface

uses Classes, AHMTypes, AHMConsts;

type
  TAHMSortDirection = (sdAscending, sdDescending);
  TAHMSortType = (stNone, stAlpha, stDate, stShuffle);

  TAHMMediaItem = class
  private
    FName: WideString;
    FCaption: String;
    FAuthor: String;
    FGroup: String;
    FCategory: String;
    FMediaType: TAHMMediaType;
    FRoot: WideString;
    FFolder: WideString;
    FFilename: WideString;
    FIcon: WideString;
    FSize: Integer;
    FDuration: Integer;
    FBitrate: Integer;
    FSequence: Integer;
    FTimestamp: TDateTime;
  protected
    function GetURL: WideString;
    function GetYear: Word;
    procedure SetYear(Year: Word);
    function GetIsMedia: Boolean;
  public
    property Name: WideString read FName write FName;
    property Caption: String read FCaption write FCaption;
    property Author: String read FAuthor write FAuthor;
    property Group: String read FGroup write FGroup;
    property Category: String read FCategory write FCategory;
    property MediaType: TAHMMediaType read FMediaType write FMediaType;
    property Root: WideString read FRoot write FRoot;
    property Folder: WideString read FFolder write FFolder;
    property Filename: WideString read FFilename write FFilename;
    property URL: WideString read GetURL;
    property Icon: WideString read FIcon write FIcon;
    property Size: Integer read FSize write FSize;
    property Duration: Integer read FDuration write FDuration;
    property Bitrate: Integer read FBitrate write FBitrate;
    property Sequence: Integer read FSequence write FSequence;
    property Timestamp: TDateTime read FTimestamp write FTimestamp;
    property Year: Word read GetYear write SetYear;
    property IsMedia: Boolean read GetIsMedia;
    procedure Assign(Source: TAHMMediaItem);
    function Duplicate: TAHMMediaItem;
  end;

  TAHMMediaList = class
  private
    FSortType: TAHMSortType;
    FSortDirection: TAHMSortDirection;
  protected
    FList: TList;
    procedure SetSortType(SortType: TAHMSortType);
    function GetCount: Integer;
    function GetItem(Index: Integer): TAHMMediaItem;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property Items[Index: Integer]: TAHMMediaItem read GetItem;
    property SortType: TAHMSortType read FSortType write SetSortType;
    property SortDirection: TAHMSortDirection read FSortDirection write FSortDirection;
    procedure Clear; virtual;
    procedure Sort;
    function AddItem(Name, Caption, Root, Folder, Filename, Icon: WideString): TAHMMediaItem; overload;
    function AddItem(Name, Caption: WideString; MediaType: TAHMMediaType; Root, Folder, Filename, Icon: WideString): TAHMMediaItem; overload;
    function AddUniqueItem(Name, Caption: WideString; MediaType: TAHMMediaType; Root, Folder, Filename, Icon: WideString): TAHMMediaItem;
    function FindItemByURL(URL: WideString): TAHMMediaItem;
  end;

  TAHMPlaylist = class(TAHMMediaList)
  private
    FIndex: Integer;
    FLoop: Boolean;
    FFinished: Boolean;
  protected
    function GetEmpty: Boolean;
    function GetHasCurrentItem: Boolean;
    function GetCanSkipNext: Boolean;
    function GetCanSkipPrev: Boolean;
    function GetCurrentItem: TAHMMediaItem;
    procedure SetCurrentItem(Item: TAHMMediaItem);
    function GetDisplayCount: String;
    function GetDisplayIndex: String;
  public
    property CurrentItem: TAHMMediaItem read GetCurrentItem write SetCurrentItem;
    property DisplayIndex: String read GetDisplayIndex;
    property DisplayCount: String read GetDisplayCount;
    property HasCurrentItem: Boolean read GetHasCurrentItem;
    property Loop: Boolean read FLoop write FLoop;
    property Finished: Boolean read FFinished;
    property CanSkipNext: Boolean read GetCanSkipNext;
    property CanSkipPrev: Boolean read GetCanSkipPrev;
    property Empty: Boolean read GetEmpty;
    procedure Clear; override;
    procedure FinishPlaylist;
    function NextItem: Boolean;
    function PreviousItem: Boolean;
    function FirstItem: Boolean;
    function LastItem: Boolean;
    procedure AppendList(List: TAHMMediaList);
    procedure InsertList(List: TAHMMediaList);
    procedure ReplaceList(List: TAHMMediaList);
  end;

  function GetMediaType(Filename: WideString): TAHMMediaType;
  procedure RegisterSupportedMediaTypes;

implementation

uses Windows, ApplicationManager, Preferences, SysUtils, StrUtils;

var
  MediaExtensions: TStringList;
  GroupDirs: Boolean; // should we group directories when sorting?

procedure RegisterSupportedMediaTypes;
var
  List: TStringList;
  i: Integer;
begin
  // Clear existing extensions
  MediaExtensions.Clear;

  List := TStringList.Create;
  try
    // Enumerate supported video file extensions from preferences manager
    List.CommaText := ApplicationManagerInstance.PreferencesManager.VideoFormats;
    for i := 0 to Pred(List.Count) do
      MediaExtensions.AddObject(List[i], TObject(Ord(mtVideo)));

    // Enumerate supported audio file extensions from preferences manager
    List.CommaText := ApplicationManagerInstance.PreferencesManager.AudioFormats;
    for i := 0 to Pred(List.Count) do
      MediaExtensions.AddObject(List[i], TObject(Ord(mtAudio)));

    // Enumerate supported playlist file extensions from preferences manager
    List.CommaText := ApplicationManagerInstance.PreferencesManager.PlaylistFormats;
    for i := 0 to Pred(List.Count) do
      MediaExtensions.AddObject(List[i], TObject(Ord(mtPlaylist)));

    // Add list of standard supported image types
    for i := Low(EXT_IMAGES) to High(EXT_IMAGES) do
      MediaExtensions.AddObject(EXT_IMAGES[i], TObject(Ord(mtImage)));

    // Add list of standard support info types
    for i := Low(EXT_INFO) to High(EXT_INFO) do
      MediaExtensions.AddObject(EXT_INFO[i], TObject(Ord(mtInfo)));

    // Add list of standard supported theme types
    MediaExtensions.AddObject(EXT_THEME, TObject(Ord(mtTheme)));
  finally
    List.Free;
  end;
end;

function GetMediaType(Filename: WideString): TAHMMediaType;
var
  Name, Ext: WideString;
  i: Integer;
begin
  // Does file end with trailing slash?
  if RightStr(Filename, 1) = PathDelim then
    Result := mtFolder
  else if RightStr(Filename, 2) = '..' then
    Result := mtParent
  else
  begin
    // Get extension of passed filename
    Name := UpperCase(Filename);
    Ext := ExtractFileExt(Name);

    // Look for extension in our list of registered types
    i := MediaExtensions.IndexOf(Ext);
    if i < 0 then
      // Not recognised, but might be a DVD image
      if Name = FILE_DVD then Result := mtDVD
                         else Result := mtUnknown
    else
      Result := TAHMMediaType(Integer(MediaExtensions.Objects[i]));
  end;
end;

//----------------------------------------------------------------------------//

function TAHMMediaItem.GetURL: WideString;
begin
  // Append filename to root and folder to generate URL
  Result := FRoot + FFolder + FFilename;
end;

function TAHMMediaItem.GetYear: Word;
begin
  // Approximate year - ignores leap years but is good enough
  Result := Round(FTimestamp / 365);
  if Result > 0 then Result := Result + 1;
end;

procedure TAHMMediaItem.SetYear(Year: Word);
begin
  // Approximate year - ignores leap years but is good enough
  if Year > 0 then FTimestamp := (Year - 1) * 365
              else FTimestamp := 0;
end;

function TAHMMediaItem.GetIsMedia: Boolean;
begin
  Result := FMediaType in [mtPlaylist, mtTheme, mtImage, mtAudio, mtRadio,
                           mtVideo, mtDVD, mtInfo, mtUnknown];
end;

procedure TAHMMediaItem.Assign(Source: TAHMMediaItem);
begin
  // Copy properties from passed media item
  FName      := Source.Name;
  FCaption   := Source.Caption;
  FAuthor    := Source.Author;
  FGroup     := Source.Group;
  FCategory  := Source.Category;
  FMediaType := Source.MediaType;
  FRoot      := Source.Root;
  FFolder    := Source.Folder;
  FFilename  := Source.Filename;
  FIcon      := Source.Icon;
  FSize      := Source.Size;
  FDuration  := Source.Duration;
  FBitrate   := Source.Bitrate;
  FSequence  := Source.Sequence;
  FTimestamp := Source.Timestamp;
end;

function TAHMMediaItem.Duplicate: TAHMMediaItem;
begin
  // Create a duplicate copy of this media item object
  Result := TAHMMediaItem.Create;
  Result.Assign(Self);
end;

//----------------------------------------------------------------------------//

constructor TAHMMediaList.Create;
begin
  inherited Create;

  FList := TList.Create;

  // Default settings
  FSortType := stAlpha;
  FSortDirection := sdAscending;
end;

destructor TAHMMediaList.Destroy;
begin
  Clear;
  FList.Free;

  inherited Destroy;
end;

procedure TAHMMediaList.SetSortType(SortType: TAHMSortType);
begin
  // If we change sort type then reset sort direction to ascending
  if SortType <> FSortType then
  begin
    FSortType := SortType;
    FSortDirection := sdAscending;
  end;
end;

function TAHMMediaList.GetCount: Integer;
begin
  Result := FList.Count;
end;

function TAHMMediaList.GetItem(Index: Integer): TAHMMediaItem;
begin
  if (Index >= 0) and (Index < FList.Count) then
    Result := TAHMMediaItem(FList.Items[Index])
  else
    Result := nil;
end;

procedure TAHMMediaList.Clear;
var
  i: Integer;
begin
  // Clear all media items too
  for i := 0 to Pred(FList.Count) do
    TAHMMediaItem(FList.Items[i]).Free;
  FList.Clear;
end;

function TAHMMediaList.AddItem(Name, Caption, Root, Folder, Filename, Icon: WideString): TAHMMediaItem;
var
  mType: TAHMMediaType;
begin
  // Determine media type from filename
  mType := GetMediaType(Root + Folder + Filename);
  Result := AddItem(Name, Caption, mType, Root, Folder, Filename, Icon);
end;

function TAHMMediaList.AddItem(Name, Caption: WideString; MediaType: TAHMMediaType; Root, Folder, Filename, Icon: WideString): TAHMMediaItem;
begin
  // Create and populate media item with passed values
  Result := TAHMMediaItem.Create;
  Result.Name := Name;
  Result.Caption := Caption;
  Result.MediaType := MediaType;
  Result.Root := Root;
  Result.Folder := Folder;
  Result.Filename := Filename;
  Result.Icon := Icon;
  FList.Add(Result);
end;

function TAHMMediaList.AddUniqueItem(Name, Caption: WideString; MediaType: TAHMMediaType; Root, Folder, Filename, Icon: WideString): TAHMMediaItem;
var
  i: Integer;
begin
  // Check that we do not already have an item with this name and type
  for i := 0 to Pred(FList.Count) do
    if (TAHMMediaItem(FList.Items[i]).Caption = Caption) and
       (TAHMMediaItem(FList.Items[i]).MediaType = MediaType) then
    begin
      // Found a duplicate, so return nil and exit
      Result := nil;
      Exit;
    end;

  // No duplicates found so add requested item
  Result := AddItem(Name, Caption, MediaType, Root, Folder, Filename, Icon);
end;

function TAHMMediaList.FindItemByURL(URL: WideString): TAHMMediaItem;
var
  i: Integer;
begin
  // See if we can locate a media item with matching URL
  for i := 0 to Pred(FList.Count) do
    if TAHMMediaItem(FList.Items[i]).URL = URL then
    begin
      Result := TAHMMediaItem(FList.Items[i]);;
      Exit;
    end;

  // Not found so return nil media item
  Result := nil;
end;


//----------------------------------------------------------------------------//
// Sorting routines to be used by the Quicksort
//----------------------------------------------------------------------------//

  // Result will be one of the following:
  //   > 0 : (positive) Item1 is less than Item2
  //     0 : Item1 is equal to Item2
  //   < 0 : (negative) Item1 is greater than Item2

// Sort list by comparing files by name (sortkey) in ascending order.
function CompareByNameAscending(Item1 : Pointer; Item2 : Pointer) : Integer;
var
  file1, file2: TAHMMediaItem;
begin
  file1 := TAHMMediaItem(Item1);
  file2 := TAHMMediaItem(Item2);

  // Ensure parent folder type is always sorted to the top
  if file1.MediaType = mtParent then Result := -1
  else if file2.MediaType = mtParent then Result := 1
  else if (file1.MediaType = mtDVD) and (file2.MediaType <> mtDVD) then Result := -1
  else if (file2.MediaType = mtDVD) and (file1.MediaType <> mtDVD) then Result := 1
  else if GroupDirs and (file1.MediaType = mtFolder) and (file2.MediaType <> mtFolder) then Result := -1
  else if GroupDirs and (file2.MediaType = mtFolder) and (file1.MediaType <> mtFolder) then Result := 1
  else Result := AnsiCompareText(file1.Name, file2.Name);
end;

// Sort list by comparing files by name (sortkey) in descending order.
function compareByNameDescending(Item1 : Pointer; Item2 : Pointer) : Integer;
var
  file1, file2: TAHMMediaItem;
begin
  file1 := TAHMMediaItem(Item1);
  file2 := TAHMMediaItem(Item2);

  // Ensure parent folder type is always sorted to the top
  if file1.MediaType = mtParent then Result := -1
  else if file2.MediaType = mtParent then Result := 1
  else if (file1.MediaType = mtDVD) and (file2.MediaType <> mtDVD) then Result := -1
  else if (file2.MediaType = mtDVD) and (file1.MediaType <> mtDVD) then Result := 1
  else if GroupDirs and (file1.MediaType = mtFolder) and (file2.MediaType <> mtFolder) then Result := -1
  else if GroupDirs and (file2.MediaType = mtFolder) and (file1.MediaType <> mtFolder) then Result := 1
  else Result := AnsiCompareText(file2.Name, file1.Name);
end;

// Sort list by comparing files by date (timestamp) in ascending order.
function compareByDateAscending(Item1 : Pointer; Item2 : Pointer) : Integer;
var
  file1, file2: TAHMMediaItem;
begin
  file1 := TAHMMediaItem(Item1);
  file2 := TAHMMediaItem(Item2);

  // Ensure parent folder type is always sorted to the top
  if file1.MediaType = mtParent then Result := -1
  else if file2.MediaType = mtParent then Result := 1
  else if (file1.MediaType = mtDVD) and (file2.MediaType <> mtDVD) then Result := -1
  else if (file2.MediaType = mtDVD) and (file1.MediaType <> mtDVD) then Result := 1
  else if GroupDirs and (file1.MediaType = mtFolder) and (file2.MediaType <> mtFolder) then Result := -1
  else if GroupDirs and (file2.MediaType = mtFolder) and (file1.MediaType <> mtFolder) then Result := 1
  else if file1.Timestamp > file2.Timestamp then Result := 1
  else if file1.Timestamp = file2.Timestamp then Result := 0
  else Result := -1;
end;

// Sort list by comparing files by date (timestamp) in descending order.
function compareByDateDescending(Item1 : Pointer; Item2 : Pointer) : Integer;
var
  file1, file2: TAHMMediaItem;
begin
  file1 := TAHMMediaItem(Item1);
  file2 := TAHMMediaItem(Item2);

  // Ensure parent folder type is always sorted to the top
  if file1.MediaType = mtParent then Result := -1
  else if file2.MediaType = mtParent then Result := 1
  else if (file1.MediaType = mtDVD) and (file2.MediaType <> mtDVD) then Result := -1
  else if (file2.MediaType = mtDVD) and (file1.MediaType <> mtDVD) then Result := 1
  else if GroupDirs and (file1.MediaType = mtFolder) and (file2.MediaType <> mtFolder) then Result := -1
  else if GroupDirs and (file2.MediaType = mtFolder) and (file1.MediaType <> mtFolder) then Result := 1
  else if file2.Timestamp > file1.Timestamp then Result := 1
  else if file2.Timestamp = file1.Timestamp then Result := 0
  else Result := -1;
end;


// Strictly speaking this is not really a sort mechanism, but its included
// here as its related to the playlist ordering.

procedure ShuffleList(List: TList);
var
  i, j: Integer;
  Item: TAHMMediaItem;
begin
  for i := Pred(List.Count) downto 1 do
  begin
    j := Random(Succ(i));

    // Don't shuffle parent items (although they shouldn't be here anyway)
    Item := TAHMMediaItem(List[j]);
    if Item.MediaType = mtParent then Continue;

    if (j <> i) then List.Exchange(i, j);
  end;
end;

//----------------------------------------------------------------------------//
// Primary Sort method.
// Sort the internal media list into ascending/descending order, by
// either name or date.
// Sort doesn't take any parameters, but uses the properties of the playlist
//----------------------------------------------------------------------------//
procedure TAHMMediaList.Sort;
begin
  // Must only sort if more than 1 item otherwise quicksort algorithm barfs
  if FList.Count > 1 then
  begin
    // Have to use a global variable to indicate grouping of directories
    GroupDirs := ApplicationManagerInstance.PreferencesManager.GroupDirectories;

    // Check to see what type of sort we should be doing. Current options are
    // None (stNone), Date (stDate), Filename (stAlpha), and shuffled (stShuffle).
    // Strictly speaking shuffle is not really a sort mechanism, but its included
    // within this method to make it easier to encapsulate the call
    case FSortType of
      stDate: if FSortDirection = sdAscending then FList.Sort(compareByDateAscending)
                                              else FList.Sort(compareByDateDescending);
      stAlpha: if FSortDirection = sdAscending then FList.Sort(compareByNameAscending)
                                                else FList.Sort(compareByNameDescending);
      stShuffle: ShuffleList(FList);
    end;
  end;
end;

//----------------------------------------------------------------------------//

function TAHMPlaylist.GetHasCurrentItem: Boolean;
begin
  Result := (FIndex <> -1);
end;

function TAHMPlaylist.GetEmpty: Boolean;
begin
  Result := (FList.Count = 0);
end;

function TAHMPlaylist.GetCanSkipNext: Boolean;
begin
  Result := (FIndex <> -1) and (FIndex < Pred(FList.Count));
end;

function TAHMPlaylist.GetCanSkipPrev: Boolean;
begin
  Result := (FIndex > 0);
end;

function TAHMPlaylist.GetCurrentItem: TAHMMediaItem;
begin
  if FFinished then Result := nil
               else Result := GetItem(FIndex);
end;

procedure TAHMPlaylist.SetCurrentItem(Item: TAHMMediaItem);
begin
  FIndex := FList.IndexOf(Item);
end;

function TAHMPlaylist.GetDisplayCount: String;
begin
  // Return empty count if playlist has finished
  if FFinished or Empty then Result := ''
                        else Result := IntToStr(GetCount);
end;

function TAHMPlaylist.GetDisplayIndex: String;
begin
  // Return empty index if playlist has finished
  if FFinished or Empty then Result := ''
                        else Result := IntToStr(Succ(FIndex));
end;

procedure TAHMPlaylist.Clear;
begin
  inherited Clear;

  // Reset playlist position and finished flag when clearing
  FIndex := -1;
  FFinished := False;
end;

procedure TAHMPlaylist.FinishPlaylist;
begin
  // Set finished flag
  FFinished := True;
end;

function TAHMPlaylist.NextItem: Boolean;
begin
  Result := False;
  if FFinished then Exit;
  Inc(FIndex);
  if FIndex < FList.Count then
    Result := True
  else if FLoop then
  begin
    FIndex := 0;
    Result := True;
  end
  else
    Dec(FIndex);
end;

function TAHMPlaylist.PreviousItem: Boolean;
begin
  Result := False;
  if FFinished then Exit;
  Dec(FIndex);
  if FIndex >= 0 then
    Result := True
  else if FLoop then
  begin
    FIndex := Pred(FList.Count);
    Result := True;
  end
  else
    Inc(FIndex);
end;

function TAHMPlaylist.FirstItem: Boolean;
begin
  if FList.Count > 0 then FIndex := 0 else FIndex := -1;
  Result := (FIndex <> -1);

  // Reset finished flag if successful
  if Result then FFinished := False;
end;

function TAHMPlaylist.LastItem: Boolean;
begin
  FIndex := Pred(FList.Count);
  Result := (FIndex <> -1);

  // Reset finished flag if successful
  if Result then FFinished := False;
end;

procedure TAHMPlaylist.AppendList(List: TAHMMediaList);
var
  i: Integer;
begin
  // Append media contents of passed playlist to our current list
  for i := 0 to Pred(List.Count) do
    if List.Items[i].IsMedia then
      FList.Add(List.Items[i].Duplicate);
end;

procedure TAHMPlaylist.InsertList(List: TAHMMediaList);
var
  i, pos: Integer;
begin
  // Insert contents of passed playlist at current index position

  // Determine insertion point - if at end of list then append instead
  if (FIndex < 0) or (FIndex >= Pred(FList.Count)) then
    AppendList(List)
  else
  begin
    Pos := Succ(FIndex);
    // Insert media contents of passed playlist in our current list
    for i := 0 to Pred(List.Count) do
      if List.Items[i].IsMedia then
      begin
        FList.Insert(Pos, List.Items[i].Duplicate);
        Inc(Pos);
      end;
  end;
end;

procedure TAHMPlaylist.ReplaceList(List: TAHMMediaList);
begin
  // Replace our current list with contents of passed playlist
  Clear;
  AppendList(List);
end;

initialization
  MediaExtensions := TStringList.Create;
  MediaExtensions.Sorted := True;
  MediaExtensions.Duplicates := dupIgnore;

finalization
  MediaExtensions.Free;

end.

